Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions lib/interscript.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,22 @@ def load(system_code, maps = {}, compiler: Interscript::Interpreter)
end

# Transliterates the string.
def transliterate(system_code, string, maps = {}, compiler: Interscript::Interpreter)
#
# ISC-WF-1 W3: `language:` selects one of the map's declared
# `languages`, activating the matching `when`-guarded rules. A
# selection outside the declaration (or no selection) leaves every
# guard inert (spec §5.1).
def transliterate(system_code, string, maps = {}, compiler: Interscript::Interpreter, language: nil)
# The current best implementation is Interpreter
load(system_code, maps, compiler: compiler).call(string)
compiled = load(system_code, maps, compiler: compiler)
if language.nil?
compiled.call(string)
elsif compiler.equal?(Interscript::Interpreter)
compiled.call(string, language: language)
else
raise ArgumentError,
"language selection is only supported by Interscript::Interpreter, not #{compiler}"
end
end

# Gives each possible value of the transliteration.
Expand Down
67 changes: 61 additions & 6 deletions lib/interscript/interpreter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ def compile(map, _: nil)
self
end

def call(str, stage = :main, each: false, &block)
def call(str, stage = :main, each: false, language: nil, &block)
stage = @map.stages[stage]
if each
e = Enumerator.new do |yielder|
Expand All @@ -22,7 +22,7 @@ def call(str, stage = :main, each: false, &block)
# standard:disable Style/GlobalVars (deliberate $DEBUG / -d-flag debug idiom)
$select_nth_string = true
# standard:enable Style/GlobalVars
result = Stage.new(@map, str).execute_rule(stage)
result = Stage.new(@map, str, language: language).execute_rule(stage)
# standard:disable Style/GlobalVars (deliberate $DEBUG / -d-flag debug idiom)
$select_nth_string = false
# standard:enable Style/GlobalVars
Expand Down Expand Up @@ -68,20 +68,72 @@ def call(str, stage = :main, each: false, &block)
e
end
else
Stage.new(@map, str).execute_rule(stage)
Stage.new(@map, str, language: language).execute_rule(stage)
end
end

# ISC-WF-1 W3: effective language for a run. A selection outside the
# map's `languages` declaration (or any selection on a map without a
# declaration) means every `when` guard is inert (spec §5.1).
def self.select_language(map, language)
return nil if language.nil?

declared = map.languages
return nil unless declared.is_a?(Interscript::Node::LanguagesDecl) &&
declared.include?(language.to_sym)

language.to_sym
end

class Stage
def initialize(map, str)
attr_reader :language

def initialize(map, str, language: nil)
@str = str
@map = map
@language = Interscript::Interpreter.select_language(map, language)
end

# ISC-WF-1 W3 guard applicability. Unguarded rules always apply; a
# guarded rule applies only under a selected language that its `when`
# list contains. Inert rules are absent from the pass (spec §5.1).
def guard_applies? rule
return true unless Interscript::Node::Rule::Sub === rule && rule.when_languages
return false unless @language

rule.when_languages.include?(@language)
end

# ISC-WF-1 W3: the effective rule set of a parallel pass = unguarded
# rules ∪ guard-applicable rules; where an unguarded rule and a
# guard-applicable rule share the same from-string, the
# guard-applicable rule wins (spec §5.2). Passes without any guard
# return the group untouched — zero change for pre-WF1 maps. Filtered
# variants are memoized per selected language on the group so the
# compiled-tree cache below stays per-language.
def resolve_parallel_guards r
return r unless r.children.any? { |c| Interscript::Node::Rule::Sub === c && c.when_languages }

r.wf1_variants ||= {}
r.wf1_variants[@language] ||= begin
applicable = r.children.select { |c| guard_applies?(c) }
guarded_froms = applicable
.select { |c| Interscript::Node::Rule::Sub === c && c.when_languages }
.map { |c| c.from.inspect }
filtered = Interscript::Node::Group::Parallel.new
filtered.children = applicable.reject do |c|
Interscript::Node::Rule::Sub === c && !c.when_languages &&
guarded_froms.include?(c.from.inspect)
end
filtered
end
end

def execute_rule r
return if r.reverse_run == true
case r
when Interscript::Node::Group::Parallel
r = resolve_parallel_guards(r)
if r.cached_tree
@str = Interscript::Stdlib.parallel_replace_tree(@str, r.cached_tree)
elsif r.subs_regexp && r.subs_replacements
Expand Down Expand Up @@ -135,9 +187,12 @@ def execute_rule r
end
when Interscript::Node::Group
r.children.each do |t|
# ISC-WF-1 W3: in sequence, an inert guarded rule is skipped.
next unless guard_applies?(t)
execute_rule(t)
end
when Interscript::Node::Rule::Sub
return @str unless guard_applies?(r)
@str = if r.to == :upcase
@str.gsub(Regexp.new(build_regexp(r)), &:upcase)
elsif r.to == :downcase
Expand All @@ -151,10 +206,10 @@ def execute_rule r
if r.stage.map
doc = @map.dep_aliases[r.stage.map].document
stage = doc.imported_stages[r.stage.name]
@str = Stage.new(doc, @str).execute_rule(stage)
@str = Stage.new(doc, @str, language: @language).execute_rule(stage)
else
stage = @map.imported_stages[r.stage.name]
@str = Stage.new(@map, @str).execute_rule(stage)
@str = Stage.new(@map, @str, language: @language).execute_rule(stage)
end
end

Expand Down
49 changes: 47 additions & 2 deletions lib/interscript/isc/document_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ def build
tests_arr = []
stages_arr = []
dependencies_arr = []
rulesets_arr = []
languages_arr = []

body.each do |item|
if item[:metadata]
Expand All @@ -45,14 +47,20 @@ def build
aliases_arr.concat(extract_aliases(item[:aliases]))
elsif item[:tests]
tests_arr.concat(extract_tests(item[:tests]))
elsif item[:languages]
# ISC-WF-1 W3: map-declared languages.
languages_arr.concat(Array(item[:languages]).map { |l| ident(l) })
elsif item[:ruleset]
# ISC-WF-1 W1: named ruleset declaration.
rulesets_arr << extract_ruleset(item)
elsif item[:stage]
stages_arr << extract_stage(item)
elsif item[:target]
dependencies_arr << extract_dependency(item)
end
end

{
doc = {
schemaVersion: SCHEMA_VERSION,
filename: @filename,
systemCode: code,
Expand All @@ -63,6 +71,11 @@ def build
stages: stages_arr,
dependencies: dependencies_arr
}
# ISC-WF-1 keys are present only when the map uses the constructs —
# documents of maps that don't keep their exact pre-WF1 shape.
doc[:languages] = languages_arr if languages_arr.any?
doc[:rulesets] = rulesets_arr if rulesets_arr.any?
doc
end

private
Expand Down Expand Up @@ -254,6 +267,15 @@ def extract_stage(item)
{name: name, body: body}
end

# ISC-WF-1 W1: a named ruleset declaration — same body shape as a
# stage, addressed by name through `use` expansion.
def extract_ruleset(item)
node = item[:ruleset]
name = ident(item[:ruleset_name])
body = Array(node).flat_map { |n| extract_stage_items(n) }
{name: name, body: body}
end

def extract_stage_items(n)
return [] unless n.is_a?(Hash)
if n[:sequence]
Expand All @@ -279,6 +301,10 @@ def extract_stage_items(n)
[{kind: :run, dependency: nil, stage: ident(n[:run_stage_only][:stage])}]
elsif n[:bare_rule]
[{kind: :bare_rule, rule: extract_rule(n[:bare_rule])}]
elsif n[:use]
# ISC-WF-1 W1/W2: `use <ident>` or `use <alias>.<ident>` with an
# optional override block.
[extract_use(n[:use])]
elsif n[:comment]
[]
elsif n[:noop]
Expand All @@ -288,6 +314,19 @@ def extract_stage_items(n)
end
end

# `use <name>` (same-map ruleset) or `use <dep>.<name>` (dependency-
# declared ruleset), plus the optional override rows (W2).
def extract_use(u)
dep = u[:qualified_name] ? ident(u[:use_alias]) : nil
name = ident(u[:qualified_name] || u[:use_alias])
item = {kind: :use, ruleset: name, dependency: dep}
overrides = Array(u[:use_overrides])
.select { |r| r.is_a?(Hash) && r.key?(:from) }
.map { |r| extract_rule(r) }
item[:overrides] = overrides if overrides.any?
item
end

# Filter out noop rules (from: None, to: None) created by empty
# parallel/sequence blocks that contain only comments or whitespace.
def filter_noop(rules)
Expand All @@ -302,11 +341,17 @@ def extract_rule(r)
from_val = r.is_a?(Hash) ? r[:from] : nil
to_val = r.is_a?(Hash) ? r[:to] : nil
constraints_val = r.is_a?(Hash) ? r[:constraints] : nil
{
rule = {
from: from_val ? materialize(from_val) : Items::None.new,
to: to_val ? materialize(to_val) : Items::None.new,
constraints: extract_constraints(constraints_val)
}
# ISC-WF-1 W3: a `when` guard folds to the list of language names.
# The rule hash never goes through Transform as a whole (from/to/
# constraints are materialized individually), so the guard fragment
# gets its own apply.
rule[:when_languages] = @transform.apply(when: r[:when]) if r.is_a?(Hash) && r[:when]
rule
end

def extract_constraints(arr)
Expand Down
9 changes: 8 additions & 1 deletion lib/interscript/isc/grammar/concerns/items.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,14 @@ module Items
str("line_end") | str("word_boundary") |
str("downcase") | str("upcase") | str("title_case") |
str("capture") | str("maybe") | str("some") | str("ref") |
str("any_character")
str("any_character") |
# ISC-WF-1: ruleset / use / override / languages / when join
# the keyword table (spec §2). Prefix matching follows the
# existing table behavior; quoted-string forms remain the
# pass-through escape hatch. Verified against the 404-map
# corpus: no bare identifier starts with any of these words.
str("ruleset") | str("use") | str("override") |
str("languages") | str("when")
end

# Concatenation: one or more atoms. The continuation pattern
Expand Down
39 changes: 37 additions & 2 deletions lib/interscript/isc/grammar/concerns/stages.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,44 @@ module Stages
braced(stage_item.repeat(0)).as(:stage)
end

# ISC-WF-1 W1: a named, reusable block of stage items. Declared at
# map top level anywhere a stage may appear; never executes on its
# own (only `use` expansion reaches the runtime).
rule(:ruleset_block) do
str("ruleset") >> whitespace >>
identifier.as(:ruleset_name) >> whitespace? >>
braced(stage_item.repeat(0)).as(:ruleset)
end

# ISC-WF-1 W1/W2: `use <ident>` (same-map ruleset) or
# `use <alias>.<ident>` (dependency-declared ruleset), with an
# optional override block of full replacement rows (child-wins on
# exact from-equality).
rule(:use_directive) do
(str("use") >> whitespace >>
identifier.as(:use_alias) >>
(str(".") >> identifier.as(:qualified_name)).maybe >>
(whitespace >> str("override") >> whitespace? >>
braced(rule_line.repeat(0)).as(:use_overrides)).maybe
).as(:use)
end

# ISC-WF-1 W3: `when <ident> (","|ws <ident>)*` — a language guard
# on a sub rule. Inert unless the caller selects a language the
# map declares (spec §5.1). Identifiers are keyword-guarded so the
# repeat stops at the next rule instead of swallowing it.
rule(:when_guard) do
(whitespace >> str("when") >> whitespace >>
keyword.absent? >> identifier.as(:when_lang) >>
((comma | whitespace) >> (keyword.absent? >> identifier.as(:when_lang))).repeat
).as(:when)
end

rule(:stage_item) do
whitespace? >>
(sequence_block | parallel_block | run_rule | separate_directive |
string_case_directive | compose_directive | rababa_directive |
bare_rule | comment_item) >>
use_directive | bare_rule | comment_item) >>
whitespace?
end

Expand Down Expand Up @@ -105,7 +138,8 @@ module Stages
str("sub") >> whitespace >>
item_atom.as(:from) >> whitespace >>
item_atom.as(:to) >>
constraints
constraints >>
when_guard.maybe
end

rule(:block_rule) do
Expand All @@ -114,6 +148,7 @@ module Stages
str("from") >> whitespace >> item.as(:from) >> whitespace? >>
str("to") >> whitespace >> item.as(:to) >>
(whitespace >> constraint).repeat.as(:constraints) >>
when_guard.maybe >>
whitespace? >> str("}")
end

Expand Down
15 changes: 13 additions & 2 deletions lib/interscript/isc/grammar/concerns/system.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,23 @@ module Concerns
module System
include Parsanol

# ISC-WF-1 W3: the languages a map serves, declared once per map
# inside the system block alongside metadata (spec §5.1). List
# identifiers are keyword-guarded so the repeat stops at the next
# block item (`stage`, `ruleset`, …) instead of swallowing it.
rule(:languages_decl) do
str("languages") >> whitespace >>
(keyword.absent? >> identifier.as(:language) >>
((comma | whitespace) >> (keyword.absent? >> identifier.as(:language))).repeat
).as(:languages)
end

# A block-item is any of the top-level constructs that may appear
# inside `system "<code>" { ... }`.
rule(:block_item) do
whitespace? >>
(metadata_block | aliases_block | tests_block |
stage_block | dependency_decl) >>
(metadata_block | aliases_block | tests_block | languages_decl |
ruleset_block | stage_block | dependency_decl) >>
whitespace?
end

Expand Down
Loading
Loading