Skip to content

Commit

Permalink
* added non-interactive mode, -k, -n, -N flags to iris-talk exe…
Browse files Browse the repository at this point in the history
…cutable

* separated session scope from [read-only] stdlib scope

* added `unset` shell command (name-scope bindings are normally permanent; this allows bindings to be discarded e.g. before re-defining a handler)

* modified VT100 formatter to embolden names defined in session scope only (user-defined commands are “more important” than built-ins)

* fixed crashing bugs in `pp`, `spp` commands
  • Loading branch information
hhas committed Feb 4, 2023
1 parent 4409436 commit d3b10cf
Show file tree
Hide file tree
Showing 13 changed files with 261 additions and 52 deletions.
2 changes: 1 addition & 1 deletion iris-glue/gluelib/gluelib_operators.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Foundation
import iris


func gluelib_loadOperators(into registry: OperatorRegistry) {
public func gluelib_loadOperators(into registry: OperatorRegistry) {

// operator keywords (minimum required to write glue definitions in native syntax)
registry.add(["swift_handler", .expression, "requires", .expression], 180)
Expand Down
2 changes: 1 addition & 1 deletion iris-glue/gluelib/operator glue template.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ private let templateSource = """
import Foundation
func stdlib_loadOperators(into registry: OperatorRegistry) {
public func stdlib_loadOperators(into registry: OperatorRegistry) {
««+loadOperators»»
registry.add(««args»»)
««-loadOperators»»
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,4 @@
uuid = "CE77B6D2-8E19-4818-944C-17777D17A76B"
type = "1"
version = "2.0">
<Breakpoints>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
<BreakpointContent
uuid = "E3437FE8-EB82-4B50-B37B-7AE7E08E0F76"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
filePath = "iris-talk/main.swift"
startingColumnNumber = "9223372036854775807"
endingColumnNumber = "9223372036854775807"
startingLineNumber = "28"
endingLineNumber = "28"
landmarkName = "runREPL()"
landmarkType = "9">
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>
73 changes: 63 additions & 10 deletions iris-talk/commands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,17 +44,30 @@ func procedure_help(command: Command, commandEnv: Scope, handler: Handler, handl
`help` – display this Help
`_` – output the result of the previous line
`_` – (underscore) output the result of the previous line
`clear` – clear screen
`commands` – list all available commands
`operators` – list all available operators
`read {prompt as optional string with_default "?"} – read next line of input from stdin, with customizable prompt
`pp {value as optional}`
– print a value;
if value is omitted, print the previous line
`say {message as string} returning string` – speak and return the given text
`spp {value as optional}`
– print a value's raw (Swift) description;
if value is omitted, print the previous line
`read {prompt as optional string with_default "?"} returning string`
– read next line of input from stdin, with customizable prompt
`say {message as string} returning string`
– speak and return the given text
`unset {name as symbol} returning anything`
– unset a value that was previously stored in the current scope
`quit` – exit the interactive shell
Expand All @@ -66,6 +79,33 @@ func procedure_help(command: Command, commandEnv: Scope, handler: Handler, handl
return nullValue
}


// `unset {name}` – remove from scope

private let type_unset_name = (
name: Symbol("unset"),
param_0: (Symbol("name"), Symbol("name"), asLiteralName),
result: asIs
)

let interface_unset_name = HandlerType(
name: type_unset_name.name,
parameters: [
nativeParameter(type_unset_name.param_0),
],
result: type_unset_name.result.nativeCoercion
)
func procedure_unset_name(command: Command, commandEnv: Scope, handler: Handler, handlerEnv: Scope, coercion: NativeCoercion) throws -> Value {
var index = 0, value: Value = nullValue
let arg_0 = try command.value(for: type_unset_name.param_0, at: &index, in: commandEnv)
if command.arguments.count > index { throw UnknownArgumentError(at: index, of: command, to: handler) }
if let env = commandEnv as? Environment {
value = env.unset(arg_0) ?? nullValue
}
return value
}


// `clear` – clear screen

let interface_clear = HandlerType(
Expand All @@ -85,15 +125,26 @@ let interface_commands = HandlerType(
parameters: [],
result: asNothing.nativeCoercion
)
func procedure_commands(command: Command, commandEnv: Scope, handler: Handler, handlerEnv: Scope, coercion: NativeCoercion) throws -> Value {
if command.arguments.count > 0 { throw UnknownArgumentError(at: 0, of: command, to: handler) }
for (name, value) in (commandEnv as! Environment).frame.sorted(by: {$0.key < $1.key}) {

func printHandlers(in env: Environment) {
for (name, value) in env.frame.sorted(by: {$0.key < $1.key}) {
if let handler = value as? Callable {
writeHelp("\(handler.interface)\n")
} else {
writeHelp("\(name.label)’ – \(value)\n")
}
}
}

func procedure_commands(command: Command, commandEnv: Scope, handler: Handler, handlerEnv: Scope, coercion: NativeCoercion) throws -> Value {
if command.arguments.count > 0 { throw UnknownArgumentError(at: 0, of: command, to: handler) }
var env = commandEnv as? Environment
while let scope = env {
print("Scope contains \(scope.frame.count) items:\n") // TO DO: how to identify/describe/name each scope? (library/handler/tell/etc)
printHandlers(in: scope)
env = scope.readOnlyParent()
print()
}
return nullValue
}

Expand All @@ -111,6 +162,7 @@ func procedure_operators(command: Command, commandEnv: Scope, handler: Handler,
if command.arguments.count > 0 { throw UnknownArgumentError(at: 0, of: command, to: handler) }
var found = Set<String>()
var precedence: Precedence = 0
// note: operator registry is shared with sub-scopes so for now there is no need to inspect parent envs
let definitions = env.operatorRegistry.patternDefinitions.map{($0.precedence, $0.name, $0.description)}
for definition in definitions.sorted(by: { ($1.0, $0.1) < ($0.0, $1.1) }) {
if !found.contains(definition.2) {
Expand Down Expand Up @@ -209,7 +261,7 @@ func procedure_say(command: Command, commandEnv: Scope, handler: Handler, handle
private let type_pp_value = (
name: Symbol("pp"),
param_0: (Symbol("value"), Symbol("value"), AsSwiftOptional(asValue)), // TO DO: this is a bit problematic: the proc below really wants Value, with nullValue rather than nil, but AsOptional doesn't compose with asValue and is in any case not a SwiftCoercion which is what's needed to unpack arguments
result: asNothing
result: asIs
)

let interface_pp_value = HandlerType(
Expand All @@ -229,14 +281,14 @@ func procedure_pp_value(command: Command, commandEnv: Scope, handler: Handler, h
if command.arguments.count > index { throw UnknownArgumentError(at: index, of: command, to: handler) }
print(" \(formatter.format(arg_0 ?? nullValue))")
}
return previousValue.get(nullSymbol) ?? nullValue
return previousValue.immutableValue
}
// `spp {value}` – print Swift representation of value/previous input; caution this may raise exception for some types (if unsupported the fallback behavior is fatalError)

private let type_spp_value = (
name: Symbol("spp"),
param_0: (Symbol("value"), Symbol("value"), AsSwiftOptional(asValue)),
result: asNothing
result: asIs
)

let interface_spp_value = HandlerType(
Expand All @@ -255,13 +307,14 @@ func procedure_spp_value(command: Command, commandEnv: Scope, handler: Handler,
if command.arguments.count > index { throw UnknownArgumentError(at: index, of: command, to: handler) }
print(" \((arg_0 ?? nullValue).swiftLiteralDescription)")
}
return previousValue.get(nullSymbol) ?? nullValue
return previousValue.immutableValue
}



func loadREPLHandlers(_ env: Environment) {
env.define(interface_help, procedure_help)
env.define(interface_unset_name, procedure_unset_name)
env.define(interface_clear, procedure_clear)
env.define(interface_commands, procedure_commands)
env.define(interface_operators, procedure_operators)
Expand Down
146 changes: 138 additions & 8 deletions iris-talk/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,19 @@ import iris


// the previous line’s result will be stored under the name `_`
let previousValue = EditableValue(nullValue, as: asAnything.nativeCoercion) // TO DO: another case of mixing Swift with Native coercions
let previousValue = EditableValue(nullValue, as: asOptional) // TO DO: another case of mixing Swift with Native coercions

var previousInput: Value = nullValue

// TO DO: REPL session should probably run in subscope, with a write barrier between session env and the top-level environment containing stdlib (will give this more thought when implementing library loader, as libraries should by default share a single read-only env instance containing stdlib as their parent to avoid unnecessary overheads)


func runREPL() {
func runREPL(parser: IncrementalParser) {
writeHelp("Welcome to the iris interactive shell. Please type `help` for assistance.")
EL_init(CommandLine.arguments[0])
let parser = IncrementalParser()
loadREPLHandlers(parser.env)
try! parser.env.set("_", to: previousValue)
let formatter = VT100TokenFormatter()
let subenv = parser.env.subscope(withWriteBarrier: true) as! Environment
let formatter = VT100TokenFormatter(env: subenv)
parser.adapterHook = { VT100Reader($0, formatter) } // install extra lexer stage
var block = "" // captures multi-line input for use in history
while isRunning {
Expand All @@ -46,8 +45,11 @@ func runREPL() {
do {
if let ast = parser.ast() { // parser has accumulated a complete single-/multi-line expression sequence
//print("PARSED:", ast)
let result = try ast.eval(in: parser.env, as: asAnything)
guard let prev = previousValue.get(nullSymbol) else { exit(5) } // bug if get() returns nil
let result = try ast.eval(in: subenv, as: asAnything)
guard let prev = previousValue.get(nullSymbol) else {
print("Expected previous value but got nil")
exit(5)
} // bug if get() returns nil
let ignoreNullResult = prev is NullValue
previousValue.set(to: result)
if let error = result as? SyntaxErrorDescription { // TO DO: this is a bit hazy as we decide exactly how to encapsulate and discover syntax errors within AST
Expand All @@ -72,5 +74,133 @@ func runREPL() {
EL_dispose()
}

runREPL()

//


func parseScript(_ code: String, parser: IncrementalParser) -> AbstractSyntaxTree? {
//print("SOURCE:" + (code.contains("\n") ? "\n" : ""), code)
//print()
parser.read(code)
if let script = parser.ast() {
print("PARSED:", script)
return script
} else {
let errors = parser.errors()
if errors.isEmpty {
let blocks = parser.incompleteBlocks()
if !blocks.isEmpty {
print("Incomplete script: \(blocks.count) block(s) need closing: \(blocks)")
}
} else {
print("Found \(errors.count) syntax error(s):")
for e in errors { print(e) }
}
return nil
}
}

func runScript(_ script: AbstractSyntaxTree, env: Environment) {
do {
let result = try script.eval(in: env, as: asAnything)
print("\nRESULT:", result)
} catch {
print(error)
}
}



//

var argCount = 1
var willRun = true
var useOperators = true
var useStdlib = true // if disabled, REPL handlers are still loaded; allows non-standard libraries to be used instead (e.g. for code generation or documentation) or, if no libraries are loaded, "data-only" behavior analogous to JSON

// TO DO: pretty-print option

func readOptions() { // TO DO: move to UserDefaults?
while argCount < CommandLine.arguments.count && CommandLine.arguments[argCount].hasPrefix("-") {
switch CommandLine.arguments[argCount] {
case "-h":
print("iris [ -h -k -n -N ] [-] [ FILE ... ]")
print()
print("Options:")
print()
print("-h -- print this help")
print()
print("-k -- check .iris files' syntax")
print()
print("-n -- do not load operator syntax")
print()
print("-N -- do not load standard library")
print()
print("If one or more FILE is given, each is run in a new sub-context.")
print()
print("In no FILE is given, starts an interactive session.")
exit(0)
case "-k":
willRun = false
case "-n":
useOperators = false
case "-N":
useStdlib = false
case "-": // end of options list
argCount += 1
return
default:
print("Unknown option \(CommandLine.arguments[argCount])")
}
argCount += 1
}
}


//
// TO DO: parser's env should be pre-initialized and passed to parser's constructor

let parser = IncrementalParser(withStdLib: false)

readOptions()


if useStdlib {
stdlib_loadHandlers(into: parser.env)
stdlib_loadConstants(into: parser.env)
if useOperators {
stdlib_loadOperators(into: parser.env.operatorRegistry)
} else {
// print("operators disabled")
}
} else {
// print("stdlib disabled")
}

loadREPLHandlers(parser.env) // for now, these are loaded in non-interactive mode as well


if argCount == CommandLine.arguments.count {
runREPL(parser: parser)
} else {

while argCount < CommandLine.arguments.count {
let path = (CommandLine.arguments[argCount] as NSString).expandingTildeInPath
let url = NSURL.fileURL(withPath: path)
if let code = try? String(contentsOf: url, encoding: .utf8) {
if (!willRun) { print("Checking file: \(path)") }
if let script = parseScript(code, parser: parser) {
if willRun {
runScript(script, env: parser.env)
} else {
print("Syntax OK")
}
} else {
print("Syntax error.")
}
} else {
print("Can't read file: \(path)")
}
argCount += 1
}
}
6 changes: 4 additions & 2 deletions iris-test/test.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import iris

func test() {

/*
let parser = IncrementalParser(withStdLib: false)
let registry = parser.env.operatorRegistry
registry.add(["shortcut_action", .expression, "requires", .expression], 180)
Expand All @@ -20,13 +21,14 @@ func test() {
registry.add([.keyword("optional"), .optional(.expression), .optional([.keyword("with_default"), .boundExpression("with_default", "default_value")])], 1500, .left)
registry.add([.keyword("record"), .optional(.boundExpression("of_type", "record_type"))], 1500, .left)

/*
do {
try parser.loadGlue(URL(fileURLWithPath: "/Users/has/swift/iris-script/shortcuts/glues/shortcut actions.iris-glue"))
try parser.loadGlue(URL(fileURLWithPath: "/Users/has/swift/iris-script/shortcuts/glues/shortcut actions.iris-glue"))
} catch {
print(error)
}
*/


//runScript(" with_default ") // stray conjunction should throw syntax error

/*
Expand Down
2 changes: 1 addition & 1 deletion libiris/coercions/optional coercions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -130,5 +130,5 @@ public struct AsOptional: NativeCoercion {
}
}

let asOptional = AsOptional(asValue)
public let asOptional = AsOptional(asValue)

Loading

0 comments on commit d3b10cf

Please sign in to comment.